package com.adee.java.jdbc;

import com.adee.java.jdbc.util.StringUtils;
import com.adee.java.jdbc.util.TelNumberGenerator;
import com.mysql.cj.NativeSession;
import com.mysql.cj.jdbc.ConnectionImpl;
import com.mysql.cj.protocol.Message;
import com.mysql.cj.protocol.Protocol;
import com.mysql.cj.protocol.a.NativeMessageBuilder;
import com.mysql.cj.protocol.a.NativePacketPayload;
import com.mysql.cj.protocol.a.NativeProtocol;
import net.sf.cglib.core.DebuggingClassWriter;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.io.InputStream;
import java.lang.reflect.*;
import java.sql.*;
import java.util.Properties;
import java.util.concurrent.ThreadLocalRandom;

/**
 * jdbc基本使用步骤（6步）：
 * 1. 注册驱动
 * 2. 获取连接
 * 3. 创建发送sql语句对象
 * 4. 发送sql语句，并获取返回结果
 * 5. 结果集解析
 * 6. 资源关闭
 */
public class JdbcTest {
    public static void main(String[] args) throws Exception {
        test2();
        // test3();
        // test4();
        // test5();
        //test6();
    }

    static {
        // 设置CGLib代理类的生成位置
        System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "./cg");
        // 设置JDK代理类的输出，生成的路径一般为com\sun\proxy$Prroxy**.class
        System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
    }


    /**
     * statement方式，存在sql注入问题
     */
    public static void test1() throws Exception {
        Connection conn1 = getConnection();
        // 3. 创建发送sql语句对象
        Statement stat = conn1.createStatement();
        String sql = "SELECT * FROM admin";
        // 4. 发送sql语句，并获取返回结果
        ResultSet rs = stat.executeQuery(sql);
        // 5. 结果集解析
        while(rs.next()) {
            System.out.println(rs.getInt("id") + "\t"
                    + rs.getString("username") + "\t"
                    + rs.getString("password"));
        }
        // 6. 资源关闭 【先开后关】
        rs.close();
        stat.close();
        conn1.close();
        System.out.println("end");
    }

    /**
     * PreparedStatement方式，对sql语句进行预编译，解决了sql注入问题，并且可以缓存sql，提升性能
     * 问题：sql语句如何进行的预编译？
     * PreparedStatement尽最大可能提高性能
     * 每一种数据库都会尽最大努力对预编译语句提供最大的性能优化。因为预编译语句有可能被重复调用，所以语句
     * 在被数据库(DB)的编译器编译后的执行代码被缓存下来,那么下次调用时只要是相同的预编译语句就不需要编译,
     * 只要将参数直接传入编译过的语句执行代码中(相当于一个函数)就会得到执行。这并不是说只有一个Connection
     * 中多次执行的预编译语句被缓存,而是对于整个DB中,只要预编译的语句语法和缓存中匹配。那么在任何时候就可以
     * 不需要再次编译而可以直接执行，而在statement的语句中,即使是相同一操作,而由于每次操作的数据不同所以使
     * 整个语句相匹配的机会极小,几乎不太可能匹配。
     *
     * 实际中，需要对jdbc连接进行池化，减少建立连接的开销，提高性能
     */
    public static void test2() throws Exception {
        Connection conn1 = getConnection();
        String sql = "SELECT * FROM admin where id > ?";
        // 3. 预编译sql
        PreparedStatement pstat = conn1.prepareStatement(sql);
        //占位符赋值
        //给占位符赋值！ 从左到右，从1开始！
        pstat.setInt(1, 3);
        //这哥们内部完成SQL语句拼接！
        //执行SQL语句即可
        ResultSet rs = pstat.executeQuery();
        // 5. 结果集解析
        while(rs.next()) {
            System.out.println(rs.getInt("id") + "\t"
                    + rs.getString("username") + "\t"
                    + rs.getString("password"));
        }
        // 6. 资源关闭 【先开后关】
        rs.close();
        // 执行preparedStatement.close()后预编译便会失效，为了预编译sql的重用，提高性能，不要关闭
        // pstat.close();
        conn1.close();
        System.out.println("end");
    }

    /**
     * insert时返回自增主键
     * 获取自增主键的原理是，在insert语句后添加一条查询语句，两条语句同时发送给数据库，一起执行。
     * 如：insert into admin (username,password) values ("TMQQ","123456");select @@identity as id;
     *
     * 上面 @@identity 是表示的是最近一次向具有identity属性(即自增列)的表插入数据时对应的自增列的值，
     * 是系统定义的全局变量。一般系统定义的全局变量都是以@@开头，用户自定义变量以@开头。
     *
     * 比如有个表A，它的自增列是id，当向A表插入一行数据后，如果插入数据后自增列的值自动增加至101，则通过
     * select @@identity得到的值就是101。使用@@identity的前提是在进行insert操作后，执行
     * select @@identity的时候连接没有关闭，否则得到的将是NULL值。
     *
     */
    public static void test3() throws Exception {
        Connection conn1 = getConnection();
        String sql = "insert into admin (username,password) values (?,?)";
        // 3. 预编译sql
        /**
         * TODO: 第二个参数填入 1 | Statement.RETURN_GENERATED_KEYS
         *       告诉statement携带回数据库生成的主键！
         */
        PreparedStatement pstat = conn1.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
        //占位符赋值
        //给占位符赋值！ 从左到右，从1开始！
        pstat.setObject(1,"TMQQ");
        pstat.setObject(2,"123456");
        //这哥们内部完成SQL语句拼接！
        //执行SQL语句即可
        int u = pstat.executeUpdate();
        System.out.println("影响行数：" + u);
        // 获取自增主键
        ResultSet rs = pstat.getGeneratedKeys();
        rs.next();
        System.out.println("主键id：" + rs.getLong(1));
        // 6. 资源关闭 【先开后关】
        // 执行preparedStatement.close()后预编译便会失效，为了预编译sql的重用，提高性能，不要关闭
        // pstat.close();
        conn1.close();
    }

    /**
     * 慢查询测试，生成10000条数据
     */
    public static void test4() throws Exception{
        Connection conn1 = getConnection();
        String sql = "insert into user1 (nickname,tel,password,reg_time) values (?,?,?,?)";
        PreparedStatement pstat = conn1.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
        for (int i = 0; i < 20000; i++) {
            pstat.setObject(1, StringUtils.getName());
            pstat.setObject(2, TelNumberGenerator.randomTel());
            pstat.setObject(3, StringUtils.getRandomString(15));
//            pstat.setObject(4,new Date(System.currentTimeMillis()-
//                    new Random().nextInt(1000*24*3600*1000)));
            long random = ThreadLocalRandom.current().nextLong(1000L*24L*3600L*1000L);
            java.util.Date d = new java.util.Date(System.currentTimeMillis()-random);
//            System.out.println(d);
            pstat.setObject(4,d);

            pstat.executeUpdate();
        }

        conn1.close();
    }

    /**
     * 批量插入，提升性能：
     *    1.url?rewriteBatchedStatements=true
     *    2.insert 语句必须使用 values
     *    3.语句后面不能添加分号;
     *    4.语句不能直接执行，每次需要装货  addBatch() 最后 executeBatch();
     *
     * 注意，一定要在url后面添加参数 ?rewriteBatchedStatements=true ，否则还是单条插入，测试如下：
     *
     * 未加 ?rewriteBatchedStatements=true ，查看mysql服务器查询日志：
     * 2023-02-05T05:15:09.601588Z	  139 Query	insert into admin (username,password) values ('TMQQ','123456')
     * 2023-02-05T05:15:09.662341Z	  139 Query	insert into admin (username,password) values ('TMQQ','123456')
     * 2023-02-05T05:15:09.691324Z	  139 Query	insert into admin (username,password) values ('TMQQ','123456')
     * 2023-02-05T05:15:09.717304Z	  139 Query	insert into admin (username,password) values ('TMQQ','123456')
     * 2023-02-05T05:15:09.747585Z	  139 Query	insert into admin (username,password) values ('TMQQ','123456')
     * 2023-02-05T05:15:09.773799Z	  139 Query	insert into admin (username,password) values ('TMQQ','123456')
     * 2023-02-05T05:15:09.800286Z	  139 Query	insert into admin (username,password) values ('TMQQ','123456')
     * 2023-02-05T05:15:09.825793Z	  139 Query	insert into admin (username,password) values ('TMQQ','123456')
     * 2023-02-05T05:15:09.861570Z	  139 Query	insert into admin (username,password) values ('TMQQ','123456')
     * 2023-02-05T05:15:09.887803Z	  139 Query	insert into admin (username,password) values ('TMQQ','123456')
     *
     * 加了 ?rewriteBatchedStatements=true ，查看mysql服务器查询日志：
     * 2023-02-05T05:18:00.127834Z	  140 Query	insert into admin (username,password) values ('TMQQ','123456'),('TMQQ','123456'),('TMQQ','123456'),('TMQQ','123456'),('TMQQ','123456'),('TMQQ','123456'),('TMQQ','123456'),('TMQQ','123456'),('TMQQ','123456'),('TMQQ','123456')
     *
     */
    public static void test5() throws Exception {
        Connection conn1 = getConnection();
        String sql = "insert into admin (username,password) values (?,?)";
        // 3. 预编译sql
        /**
         * TODO: 第二个参数填入 1 | Statement.RETURN_GENERATED_KEYS
         *       告诉statement携带回数据库生成的主键！
         */
        PreparedStatement pstat = conn1.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
        for (int i = 0; i < 10; i++) {
            pstat.setObject(1,"TMQQ");
            pstat.setObject(2,"123456");
            pstat.addBatch(); // 加入批量
        }
        // 执行批量
        pstat.executeBatch();

        conn1.close();
    }

    /**
     * 事务
     * // 场景
     *    一个业务涉及多条修改数据库语句!
     *    例如: 经典的转账案例,转账业务(加钱和减钱)
     *          批量删除(涉及多个删除)
     *          批量添加(涉及多个插入)
     *
     * // 事务特性
     *   1. 原子性（Atomicity）原子性是指事务是一个不可分割的工作单位，事务中的操作要么都发生，要么都不发生。
     *
     *   2. 一致性（Consistency）事务必须使数据库从一个一致性状态变换到另外一个一致性状态。
     *
     *   3. 隔离性（Isolation）事务的隔离性是指一个事务的执行不能被其他事务干扰，即一个事务内部的操作及使用的数据对并发的其他事务是隔离的，并发执行的各个事务之间不能互相干扰。
     *
     *   4. 持久性（Durability）持久性是指一个事务一旦被提交，它对数据库中数据的改变就是永久性的，接下来的其他操作和数据库故障不应该对其有任何影响
     *
     * // 事务类型
     *
     *   自动提交 : 每条语句自动存储一个事务中,执行成功自动提交,执行失败自动回滚! (MySQL)
     *   手动提交:  手动开启事务,添加语句,手动提交或者手动回滚即可!
     *
     * // sql开启事务方式
     *    针对自动提交: 关闭自动提交即可,多条语句添加以后,最终手动提交或者回滚! (推荐)
     *
     *       SET autocommit = off; //关闭当前连接自动事务提交方式
     *       # 只有当前连接有效
     *       # 编写SQL语句即可
     *       SQL
     *       SQL
     *       SQL
     *       #手动提交或者回滚 【结束当前的事务】
     *       COMMIT / ROLLBACK ;
     *
     *    手动开启事务: 开启事务代码,添加SQL语句,事务提交或者事务回滚! (不推荐)
     */
    public static void test6() throws Exception {
        Connection conn = getConnection();
        conn.setAutoCommit(false); //关闭自动提交了

        //注意,只要当前connection对象,进行数据库操作,都不会自动提交事务
        //数据库动作!
        //statement - 单一的数据库动作 c u r d
        String sql = "insert into admin (username,password) values (?,?)";
        // 3. 预编译sql
        /**
         * TODO: 第二个参数填入 1 | Statement.RETURN_GENERATED_KEYS
         *       告诉statement携带回数据库生成的主键！
         */
        PreparedStatement pstat = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
        for (int i = 0; i < 10; i++) {
            pstat.setObject(1,"TMQQ");
            pstat.setObject(2,"123456");
            pstat.addBatch(); // 加入批量
        }
        // 执行批量
        pstat.executeBatch();
        // 回滚事务
        conn.rollback();
        // 提交事务
        conn.commit();
    }

    public static Connection getConnection() throws Exception {
        // 0.读取配置文件中的4个基本信息
        // 当前类.class.获取类加载器.获取输入流的文件名
        // 自定义类是由系统类加载器加载的
        InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream("jdbc.properties");

        Properties pros = new Properties();
        pros.load(is);

        String user = pros.getProperty("user");
        String password = pros.getProperty("password");
        String url = pros.getProperty("url");
        String driverClass = pros.getProperty("driverClass");

        //1.注册驱动
        // 方案1: 调用静态方法,但是会注册两次
        // DriverManager.registerDriver(new com.mysql.cj.jdbc.Driver());
        // 方案2: 反射触发
        Class.forName(driverClass); // 其内部static代码块也是调用 DriverManager.registerDriver(new com.mysql.cj.jdbc.Driver());

        // 2.获取连接
        // 做的工作是：建立socket连接，登录认证，从服务器查询一些配置参数
        Connection conn = DriverManager.getConnection(url, user, password);
        //proxy(conn);
        //可手动设置conn的事务隔离级别，默认与数据库设置一致
        // conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
        return conn;
    }

    // 给conn.session.protocol.commandBuilder对象创建一个代理，为了打印发送的命令
    public static void proxy(Connection conn) throws NoSuchFieldException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
        // Protocol<? extends Message> protocol
        Class<ConnectionImpl> clazz = (Class<ConnectionImpl>) conn.getClass();
        Field sessionField = clazz.getDeclaredField("session");
        sessionField.setAccessible(true);
        NativeSession ns = (NativeSession)sessionField.get(conn);

        Class<? extends NativeSession> nsClass = ns.getClass();
        Field protocolField = nsClass.getSuperclass().getDeclaredField("protocol");
        protocolField.setAccessible(true);
        NativeProtocol protocol = (NativeProtocol)protocolField.get(ns);


        Class<? extends NativeProtocol> protocolClass = protocol.getClass();
        Method getCommandBuilderMethod = protocolClass.getDeclaredMethod("getCommandBuilder");
        getCommandBuilderMethod.setAccessible(true);
        getCommandBuilderMethod.invoke(protocol);
        Field commandBuilderField = protocolClass.getDeclaredField("commandBuilder");
        commandBuilderField.setAccessible(true);
        NativeMessageBuilder commandBuilder = (NativeMessageBuilder)commandBuilderField.get(protocol);
        // 生成 NativeSession.protocol 的代理
        // jdk动态代理，此处不能使用jdk动态代理
        // Protocol proxy = JdkProxyUtil.getProxy(Protocol.class, new MyInvocationHandler(protocol));
        // cglib动态代理
        // 默认调用无参构造，此处需要有参构造
        //NativeProtocol proxy = (NativeProtocol)Enhancer.create(NativeProtocol.class, new MyInterceptor(protocol));
        Enhancer e = new Enhancer();
        e.setSuperclass(NativeMessageBuilder.class);
        e.setCallback(new MyInterceptor2(commandBuilder));
        NativeMessageBuilder proxy = (NativeMessageBuilder)e.create(
                new Class[]{boolean.class}, new Object[]{true});
        // 将目标对象属性设置到代理对象
        copyProps(commandBuilder, proxy);
        commandBuilderField.set(protocol, proxy);
    }

    // 将目标对象属性设置到代理对象
    public static void copyProps(Object from, Object to) throws IllegalAccessException {
        Class clazz = from.getClass();
        // 遍历当前类所有属性
        for (Field f : clazz.getDeclaredFields()) {
            if(!Modifier.isFinal(f.getModifiers())) {
                f.setAccessible(true);
                f.set(to, f.get(from));
            }

        }
        // 遍历父类所有属性
        if(clazz.getSuperclass() != Object.class) {
            for (Field f : clazz.getSuperclass().getDeclaredFields()) {
                if(!Modifier.isFinal(f.getModifiers())) {
                    f.setAccessible(true);
                    f.set(to, f.get(from));
                }

            }
        }
    }

    static class MyInvocationHandler implements InvocationHandler {
        private Protocol<? extends Message> target;

        public MyInvocationHandler(Protocol<? extends Message> target) {
            this.target = target;
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            if("sendCommand".equals(method.getName())) {
                Message queryPacket = (Message)args[0];
                System.out.println("发送命令：");
                System.out.println(queryPacket.toString());
            }
            return method.invoke(target, args);
        }
    }

    // 定义一个cglib方法拦截器，即代理逻辑。基本类似于jdk的InvocationHandler
    static class MyInterceptor implements MethodInterceptor {
        private NativeProtocol target;

        public MyInterceptor(NativeProtocol target) {
            this.target = target;
        }

        @Override
        public Object intercept(Object o, Method method, Object[] args, MethodProxy proxy) throws Throwable {
            System.out.println(method.getName());
            if("commandBuilder".equals(method.getName())) {
                Message queryPacket = (Message)args[0];
                System.out.println("发送命令：");
                System.out.println(queryPacket.toString());
            }
            return method.invoke(target, args);
        }
    }

    static class MyInterceptor2 implements MethodInterceptor {
        private NativeMessageBuilder target;

        public MyInterceptor2(NativeMessageBuilder target) {
            this.target = target;
        }

        @Override
        public Object intercept(Object o, Method method, Object[] args, MethodProxy proxy) throws Throwable {
            System.out.println(method.getName());
            Object invoke = method.invoke(target, args);
            if (invoke instanceof NativePacketPayload) {
                System.out.println("-->> " + invoke.toString());
            }
            return invoke;
        }
    }
}
